/*-------------------------------------------------------------------------
 *
 * portalmem.c
 *	  后端门户内存管理
 *
 * 门户是表示查询执行状态的对象。此模块为门户提供内存管理服务，但实际上并不运行
 * 供其使用的执行器。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/utils/mmgr/portalmem.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/portalcmds.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "storage/ipc.h"
#include "utils/builtins.h"
#include "utils/memutils.h"
#include "utils/snapmgr.h"
#include "utils/timestamp.h"

/*
 * 估算用户可能打开的最大门户数量， 
 * 用于在 EnablePortalManager() 中初始调整 PortalHashTable 的大小。
 * 由于哈希表可以扩展，因此无需过于慷慨，保持它的小型
 * 避免在事务结束期间执行的 hash_seq_search() 调用中出现不必要的开销。
 */
#define PORTALS_PER_USER	   16


/* ----------------
 *		全局状态
 * ----------------
 */

#define MAX_PORTALNAME_LEN		NAMEDATALEN

typedef struct portalhashent
{
	char		portalname[MAX_PORTALNAME_LEN];
	Portal		portal;
} PortalHashEnt;

static HTAB *PortalHashTable = NULL;

#define PortalHashTableLookup(NAME, PORTAL) \
do { \
	PortalHashEnt *fc_hentry; \
	\
	fc_hentry = (PortalHashEnt *) hash_search(PortalHashTable, \
										   (NAME), HASH_FIND, NULL); \
	if (fc_hentry) \
		PORTAL = fc_hentry->portal; \
	else \
		PORTAL = NULL; \
} while(0)

#define PortalHashTableInsert(PORTAL, NAME) \
do { \
	PortalHashEnt *fc_hentry; bool fc_found; \
	\
	fc_hentry = (PortalHashEnt *) hash_search(PortalHashTable, \
										   (NAME), HASH_ENTER, &fc_found); \
	if (fc_found) \
		elog(ERROR, "duplicate portal name"); \
	fc_hentry->portal = PORTAL; \
	/* 为避免重复存储，使 PORTAL->name 指向 htab 条目 */ \
	PORTAL->name = fc_hentry->portalname; \
} while(0)

#define PortalHashTableDelete(PORTAL) \
do { \
	PortalHashEnt *fc_hentry; \
	\
	fc_hentry = (PortalHashEnt *) hash_search(PortalHashTable, \
										   PORTAL->name, HASH_REMOVE, NULL); \
	if (fc_hentry == NULL) \
		elog(WARNING, "trying to delete portal name that does not exist"); \
} while(0)

static MemoryContext TopPortalContext = NULL;


/* ----------------------------------------------------------------
 *				   公共门户接口函数
 * ----------------------------------------------------------------
 */

/*
 * EnablePortalManager
 *		在后端启动时启用门户管理模块。
 */
void EnablePortalManager(void)
{
	HASHCTL		fc_ctl;

	Assert(TopPortalContext == NULL);

	TopPortalContext = AllocSetContextCreate(TopMemoryContext,
											 "TopPortalContext",
											 ALLOCSET_DEFAULT_SIZES);

	fc_ctl.keysize = MAX_PORTALNAME_LEN;
	fc_ctl.entrysize = sizeof(PortalHashEnt);

	/*
	 * 使用 PORTALS_PER_USER 作为初步创建哈希表条目的估计。
	 */
	PortalHashTable = hash_create("Portal hash", PORTALS_PER_USER,
								  &fc_ctl, HASH_ELEM | HASH_STRINGS);
}

/*
 * GetPortalByName
 *		根据门户名称返回门户，如果未找到名称则返回 NULL。
 */
Portal GetPortalByName(const char *fc_name)
{
	Portal		fc_portal;

	if (PointerIsValid(fc_name))
		PortalHashTableLookup(fc_name, fc_portal);
	else
		fc_portal = NULL;

	return fc_portal;
}

/*
 * PortalGetPrimaryStmt
 *		获取门户内的“主要”语句，即标记为 canSetTag 的那个。
 *
 * 如果没有这样的语句则返回 NULL。如果门户内的多个 PlannedStmt 
 * 结构标记为 canSetTag，则返回第一个。当前使用此函数的情况下，不应该出现这两种情况。
 */
PlannedStmt * PortalGetPrimaryStmt(Portal fc_portal)
{
	ListCell   *fc_lc;

	foreach(fc_lc, fc_portal->stmts)
	{
		PlannedStmt *fc_stmt = lfirst_node(PlannedStmt, fc_lc);

		if (fc_stmt->canSetTag)
			return fc_stmt;
	}
	return NULL;
}

/*
 * CreatePortal
 *		根据名称返回一个新的门户。
 *
 * allowDup：如果为 true，则自动丢弃任何同名的预先存在的门户 
 *（如果为 false，则引发错误）。
 *
 * dupSilent：如果为 true，则甚至不发出 WARNING。
 */
Portal CreatePortal(const char *fc_name, bool fc_allowDup, bool fc_dupSilent)
{
	Portal		fc_portal;

	AssertArg(PointerIsValid(fc_name));

	fc_portal = GetPortalByName(fc_name);
	if (PortalIsValid(fc_portal))
	{
		if (!fc_allowDup)
			ereport(ERROR,
					(errcode(ERRCODE_DUPLICATE_CURSOR),
					 errmsg("cursor \"%s\" already exists", fc_name)));
		if (!fc_dupSilent)
			ereport(WARNING,
					(errcode(ERRCODE_DUPLICATE_CURSOR),
					 errmsg("closing existing cursor \"%s\"",
							fc_name)));
		PortalDrop(fc_portal, false);
	}

	/* 创建新的门户结构 */
	fc_portal = (Portal) MemoryContextAllocZero(TopPortalContext, sizeof *fc_portal);

	/* 初始化门户上下文；通常它不会存储太多 */
	fc_portal->portalContext = AllocSetContextCreate(TopPortalContext,
												  "PortalContext",
												  ALLOCSET_SMALL_SIZES);

	/* 为门户创建资源所有者 */
	fc_portal->resowner = ResourceOwnerCreate(CurTransactionResourceOwner,
										   "Portal");

	/* 初始化未从零开始的门户字段 */
	fc_portal->status = PORTAL_NEW;
	fc_portal->cleanup = PortalCleanup;
	fc_portal->createSubid = GetCurrentSubTransactionId();
	fc_portal->activeSubid = fc_portal->createSubid;
	fc_portal->createLevel = GetCurrentTransactionNestLevel();
	fc_portal->strategy = PORTAL_MULTI_QUERY;
	fc_portal->cursorOptions = CURSOR_OPT_NO_SCROLL;
	fc_portal->atStart = true;
	fc_portal->atEnd = true;		/* 在查询设置之前不允许获取 */
	fc_portal->visible = true;
	fc_portal->creation_time = GetCurrentStatementStartTimestamp();

	/* 将门户放入表中（设置 portal->name） */
	PortalHashTableInsert(fc_portal, fc_name);

	/* 对于命名的门户重用 portal->name 副本 */
	MemoryContextSetIdentifier(fc_portal->portalContext, fc_portal->name[0] ? fc_portal->name : "<unnamed>");

	return fc_portal;
}

/*
 * CreateNewPortal
 *		创建一个新的门户，为其分配一个随机的不冲突名称。
 */
Portal CreateNewPortal(void)
{
	static unsigned int fc_unnamed_portal_count = 0;

	char		fc_portalname[MAX_PORTALNAME_LEN];

	/* 选择一个不冲突的名称 */
	for (;;)
	{
		fc_unnamed_portal_count++;
		sprintf(fc_portalname, "<unnamed portal %u>", fc_unnamed_portal_count);
		if (GetPortalByName(fc_portalname) == NULL)
			break;
	}

	return CreatePortal(fc_portalname, false, false);
}

/*
 * PortalDefineQuery
 *		建立门户查询的简单子例程。
 *
 * 注意：从 PG 8.4 开始，调用者必须提供 sourceText 字符串；不再允许
 * 传递 NULL。（如果你确实没有源文本，可以传递一个常量字符串，或许是
 * "(query not available)"）。
 *
 * commandTag 仅当且仅当原始查询字符串（重写之前）为空字符串时应为 NULL。
 * 此外，传递的 commandTag 必须是一个指向常量字符串的指针，因为它不会被复制。
 *
 * 如果提供了 cplan，那么它是一个包含语句的缓存计划，调用者必须已经
 * 调用 GetCachedPlan()，导致引用计数增加。
 * 当门户被销毁时，引用计数将被释放。
 *
 * 如果 cplan 为 NULL，那么确保传递的计划树具有足够生命周期的是
 * 调用者的责任。通常，通过将它们复制到门户的上下文中来做到这一点。
 *
 * 调用者还负责确保传递的 prepStmtName（如果不为 NULL）和
 * sourceText 具有足够的生命周期。
 *
 * 注意：此函数不应做太多超出存储传递值的操作；特别是不要进行任何可能
 * 导致 elog(ERROR) 的操作。如果在此处发生这种情况并存储 cplan 引用之前
 * 引发错误，我们将泄漏调用者试图交给我们的 plancache 引用计数。
 */
void PortalDefineQuery(Portal fc_portal,
				  const char *fc_prepStmtName,
				  const char *fc_sourceText,
				  CommandTag fc_commandTag,
				  List *fc_stmts,
				  CachedPlan *fc_cplan)
{
	AssertArg(PortalIsValid(fc_portal));
	AssertState(fc_portal->status == PORTAL_NEW);

	AssertArg(fc_sourceText != NULL);
	AssertArg(fc_commandTag != CMDTAG_UNKNOWN || fc_stmts == NIL);

	fc_portal->prepStmtName = fc_prepStmtName;
	fc_portal->sourceText = fc_sourceText;
	fc_portal->qc.commandTag = fc_commandTag;
	fc_portal->qc.nprocessed = 0;
	fc_portal->commandTag = fc_commandTag;
	fc_portal->stmts = fc_stmts;
	fc_portal->cplan = fc_cplan;
	fc_portal->status = PORTAL_DEFINED;
}

/*
 * PortalReleaseCachedPlan
 *		释放门户对其缓存计划的引用（如果有）。
 */
static void fc_PortalReleaseCachedPlan(Portal fc_portal)
{
	if (fc_portal->cplan)
	{
		ReleaseCachedPlan(fc_portal->cplan, NULL);
		fc_portal->cplan = NULL;

		/*
		 * 我们还必须清除 portal->stmts，它现在是对缓存计划的计划列表的悬空引用。
		 * 这保护了任何可能稍后尝试检查 Portal 的代码。
		 */
		fc_portal->stmts = NIL;
	}
}

/*
 * PortalCreateHoldStore
 *		为门户创建元组存储。
 */
void PortalCreateHoldStore(Portal fc_portal)
{
	MemoryContext fc_oldcxt;

	Assert(fc_portal->holdContext == NULL);
	Assert(fc_portal->holdStore == NULL);
	Assert(fc_portal->holdSnapshot == NULL);

	/*
	 * 创建用于存储元组集的内存上下文。
	 * 注意，这不是门户的 portalContext 的子项。
	 */
	fc_portal->holdContext =
		AllocSetContextCreate(TopPortalContext,
							  "PortalHoldContext",
							  ALLOCSET_DEFAULT_SIZES);

	/*
	 * 创建元组存储，选择跨事务的临时文件，并且仅在光标需要滚动时启用随机访问。
	 *
	 * XXX：应该为门户大小使用 maintenance_work_mem 吗？
	 */
	fc_oldcxt = MemoryContextSwitchTo(fc_portal->holdContext);

	fc_portal->holdStore =
		tuplestore_begin_heap(fc_portal->cursorOptions & CURSOR_OPT_SCROLL,
							  true, work_mem);

	MemoryContextSwitchTo(fc_oldcxt);
}

/*
 * PinPortal
 *		保护一个门户不被丢弃。
 *
 * 被固定的门户在事务或子事务中中止时仍然会被解锁和丢弃。
 */
void PinPortal(Portal fc_portal)
{
	if (fc_portal->portalPinned)
		elog(ERROR, "portal already pinned");

	fc_portal->portalPinned = true;
}

void UnpinPortal(Portal fc_portal)
{
	if (!fc_portal->portalPinned)
		elog(ERROR, "portal not pinned");

	fc_portal->portalPinned = false;
}

/*
 * MarkPortalActive
 *		将一个门户的状态从 READY 转换为 ACTIVE。
 *
 * 注意：永远不要直接设置 portal->status = PORTAL_ACTIVE；请调用此函数。
 */
void MarkPortalActive(Portal fc_portal)
{
	/* 为了安全，这是一个运行时测试，而不仅仅是一个断言 */
	if (fc_portal->status != PORTAL_READY)
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("portal \"%s\" cannot be run", fc_portal->name)));
	/* 执行状态转换 */
	fc_portal->status = PORTAL_ACTIVE;
	fc_portal->activeSubid = GetCurrentSubTransactionId();
}

/*
 * MarkPortalDone
 *		将一个 portal 从 ACTIVE 转换为 DONE 状态。
 *
 * 注意：绝不要直接设置 portal->status = PORTAL_DONE; 请调用这个函数。
 */
void MarkPortalDone(Portal fc_portal)
{
	/* 执行状态转换 */
	Assert(fc_portal->status == PORTAL_ACTIVE);
	fc_portal->status = PORTAL_DONE;

	/*
	 * 允许 portalcmds.c 清理它知道的状态。我们不妨现在就这么做，
	 * 因为该 portal 不能再执行了。
	 *
	 * 在某些涉及在已回滚事物中执行 ROLLBACK 命令的情况下，这是必要的，
	 * 否则我们会在执行清理钩子时到达 AtCleanup_Portals。
	 */
	if (PointerIsValid(fc_portal->cleanup))
	{
		fc_portal->cleanup(fc_portal);
		fc_portal->cleanup = NULL;
	}
}

/*
 * MarkPortalFailed
 *		将一个 portal 转换为 FAILED 状态。
 *
 * 注意：绝不要直接设置 portal->status = PORTAL_FAILED; 请调用这个函数。
 */
void MarkPortalFailed(Portal fc_portal)
{
	/* 执行状态转换 */
	Assert(fc_portal->status != PORTAL_DONE);
	fc_portal->status = PORTAL_FAILED;

	/*
	 * 允许 portalcmds.c 清理它知道的状态。我们不妨现在就这么做，
	 * 因为该 portal 不能再执行了。
	 *
	 * 在某些涉及清理已回滚事务的情况下，这是必要的，
	 * 否则我们会在执行清理钩子时到达 AtCleanup_Portals。
	 */
	if (PointerIsValid(fc_portal->cleanup))
	{
		fc_portal->cleanup(fc_portal);
		fc_portal->cleanup = NULL;
	}
}

/*
 * PortalDrop
 *		销毁该 portal。
 */
void PortalDrop(Portal fc_portal, bool fc_isTopCommit)
{
	AssertArg(PortalIsValid(fc_portal));

	/*
	 * 不允许删除一个已固定的 portal，因为那仍然被固定它的某个事务所需。
	 */
	if (fc_portal->portalPinned)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_CURSOR_STATE),
				 errmsg("cannot drop pinned portal \"%s\"", fc_portal->name)));

	/*
	 * 不确定 PORTAL_ACTIVE 情况是否可以有效发生...
	 */
	if (fc_portal->status == PORTAL_ACTIVE)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_CURSOR_STATE),
				 errmsg("cannot drop active portal \"%s\"", fc_portal->name)));

	/*
	 * 允许 portalcmds.c 清理它知道的状态，特别是如果仍然活动则关闭执行器。
	 * 这一步可能会运行用户定义的代码，因此需要预期会失败。
	 * 清理钩子的责任是不要在失败发生后尝试多次执行这一步，
	 * 然后在事务回滚期间再回到删除该 portal。
	 *
	 * 注意：在大多数控制路径中，这将在 MarkPortalDone 或 MarkPortalFailed 中已完成。
	 * 我们只是确保这一点。
	 */
	if (PointerIsValid(fc_portal->cleanup))
	{
		fc_portal->cleanup(fc_portal);
		fc_portal->cleanup = NULL;
	}

	/* 此时不应该有活动快照，除非在错误发生后 */
	Assert(fc_portal->portalSnapshot == NULL || !fc_isTopCommit);

	/*
	 * 从哈希表中移除 portal。因为我们在这里做，所以如果后续步骤中有任何错误，
	 * 我们将不会再回来尝试移除 portal。泄露一点内存总比进入无限错误恢复循环要好。
	 */
	PortalHashTableDelete(fc_portal);

	/* 删除缓存的计划引用（如果有的话） */
	fc_PortalReleaseCachedPlan(fc_portal);

	/*
	 * 如果 portal 有一个保护其数据的快照，则释放该快照。由于注册将附加到
	 * portal 的资源所有者，这需要一点小心；如果 portal 失败了，
	 * 我们将在事务回滚期间已释放资源所有者（以及快照）。
	 */
	if (fc_portal->holdSnapshot)
	{
		if (fc_portal->resowner)
			UnregisterSnapshotFromOwner(fc_portal->holdSnapshot,
										fc_portal->resowner);
		fc_portal->holdSnapshot = NULL;
	}

	/*
	 * 释放仍附加到 portal 的任何资源。这里有几种情况被涵盖：
	 *
	 * 顶级事务提交（通过 isTopCommit 指示）：通常在这里我们不应该做任何事情，
	 * 让常规的事务结束资源释放机制来处理这些资源。
	 * 然而，如果我们有一个 FAILED 的 portal（例如，遇到错误的游标），
	 * 我们最好清理其资源以避免资源泄露警告信息。
	 *
	 * 子事务提交：根本不会来到这里，因为我们不杀死
	 * AtSubCommit_Portals() 中的任何 portal。
	 *
	 * 主事务或子事务回滚：我们在这里什么也不做，因为 portal->resowner 已经设置为 NULL；
	 * 这些资源在事务回滚中已经清理掉了。
	 *
	 * 普通 portal 删除：必须释放资源。然而，如果 portal 不是 FAILED，
	 * 则我们不会释放它的锁。这些锁变成事务资源所有者的责任（因为
	 * 它是 portal 所有者的父对象），并将在事务最终结束时释放。
	 */
	if (fc_portal->resowner &&
		(!fc_isTopCommit || fc_portal->status == PORTAL_FAILED))
	{
		bool		fc_isCommit = (fc_portal->status != PORTAL_FAILED);

		ResourceOwnerRelease(fc_portal->resowner,
							 RESOURCE_RELEASE_BEFORE_LOCKS,
							 fc_isCommit, false);
		ResourceOwnerRelease(fc_portal->resowner,
							 RESOURCE_RELEASE_LOCKS,
							 fc_isCommit, false);
		ResourceOwnerRelease(fc_portal->resowner,
							 RESOURCE_RELEASE_AFTER_LOCKS,
							 fc_isCommit, false);
		ResourceOwnerDelete(fc_portal->resowner);
	}
	fc_portal->resowner = NULL;

	/*
	 * 如果存在，删除 tuplestore。即使在错误情况下也应执行此操作；
	 * 由于 tuplestore 使用的是跨事务存储，其临时文件需要显式删除。
	 */
	if (fc_portal->holdStore)
	{
		MemoryContext fc_oldcontext;

		fc_oldcontext = MemoryContextSwitchTo(fc_portal->holdContext);
		tuplestore_end(fc_portal->holdStore);
		MemoryContextSwitchTo(fc_oldcontext);
		fc_portal->holdStore = NULL;
	}

	/* 删除 tuplestore 存储（如果有的话） */
	if (fc_portal->holdContext)
		MemoryContextDelete(fc_portal->holdContext);

	/* 释放附属存储 */
	MemoryContextDelete(fc_portal->portalContext);

	/* 释放 portal 结构（它在 TopPortalContext 中） */
	pfree(fc_portal);
}

/*
 * 删除所有声明的游标。
 *
 * 用于命令：CLOSE ALL, DISCARD ALL
 */
void PortalHashTableDeleteAll(void)
{
	HASH_SEQ_STATUS fc_status;
	PortalHashEnt *fc_hentry;

	if (PortalHashTable == NULL)
		return;

	hash_seq_init(&fc_status, PortalHashTable);
	while ((fc_hentry = hash_seq_search(&fc_status)) != NULL)
	{
		Portal		fc_portal = fc_hentry->portal;

		/* 不能关闭活动的 portal（正在运行命令的那个） */
		if (fc_portal->status == PORTAL_ACTIVE)
			continue;

		PortalDrop(fc_portal, false);

		/* 在可能导致其他删除的情况下重新开始迭代 */
		hash_seq_term(&fc_status);
		hash_seq_init(&fc_status, PortalHashTable);
	}
}

/*
 * “保持”一个 portal。为后续事务的访问做准备。
 */
static void fc_HoldPortal(Portal fc_portal)
{
	/*
	 * 请注意，PersistHoldablePortal() 必须释放由 portal 使用的所有本地于创建事务的资源。
	 */
	PortalCreateHoldStore(fc_portal);
	PersistHoldablePortal(fc_portal);

	/* 删除缓存的计划引用（如果有的话） */
	fc_PortalReleaseCachedPlan(fc_portal);

	/*
	 * 属于 portal 的任何资源将在即将进行的全事务清理中释放；
	 * portal 将不再拥有自己的资源。
	 */
	fc_portal->resowner = NULL;

	/*
	 * 成功导出可持有的光标后，将其标记为不属于该事务。
	 */
	fc_portal->createSubid = InvalidSubTransactionId;
	fc_portal->activeSubid = InvalidSubTransactionId;
	fc_portal->createLevel = 0;
}

/*
 * 门户的预提交处理。
 *
 * 在此事务中创建的可持有光标需要转换为物化形式，因为我们将关闭执行器并释放锁。
 * 在此事务中创建的非可持有门户将被简单移除。来自先前事务的门户应保持不变。
 *
 * 如果任何门户更改状态（可能导致用户自定义代码运行），则返回真；否则返回假。
 */
bool PreCommit_Portals(bool fc_isPrepare)
{
	bool		fc_result = false;
	HASH_SEQ_STATUS fc_status;
	PortalHashEnt *fc_hentry;

	hash_seq_init(&fc_status, PortalHashTable);

	while ((fc_hentry = (PortalHashEnt *) hash_seq_search(&fc_status)) != NULL)
	{
		Portal		fc_portal = fc_hentry->portal;

		/*
		 * 应该不再有固定的门户。如果有人泄漏一个，请投诉。允许自动保持的门户；我们假设固定它们的人正在管理它们。
		 */
		if (fc_portal->portalPinned && !fc_portal->autoHeld)
			elog(ERROR, "cannot commit while a portal is pinned");

		/*
		 * 不要触碰活动门户 --- 这只能在多事务实用程序命令（例如 VACUUM）或过程中的提交情况下发生。
		 *
		 * 然而，请注意，附加到此类门户的任何资源所有者仍将消失，因此不要留下悬挂指针。还要注销门户持有的任何快照，主要是为了避免 ResourceOwnerRelease() 的快照泄漏警告。
		 */
		if (fc_portal->status == PORTAL_ACTIVE)
		{
			if (fc_portal->holdSnapshot)
			{
				if (fc_portal->resowner)
					UnregisterSnapshotFromOwner(fc_portal->holdSnapshot,
												fc_portal->resowner);
				fc_portal->holdSnapshot = NULL;
			}
			fc_portal->resowner = NULL;
			/* 为了清洁起见，也清除 portalSnapshot */
			fc_portal->portalSnapshot = NULL;
			continue;
		}

		/* 是否在当前事务中创建了可持有的门户？ */
		if ((fc_portal->cursorOptions & CURSOR_OPT_HOLD) &&
			fc_portal->createSubid != InvalidSubTransactionId &&
			fc_portal->status == PORTAL_READY)
		{
			/*
			 * 我们正在退出创建可持有游标的事务。
			 * 不要丢弃门户，而是准备它以供后续事务访问。
			 *
			 * 不过，如果这是 PREPARE TRANSACTION 而不是 COMMIT，
			 * 就拒绝 PREPARE，因为语义似乎相当不明确。
			 */
			if (fc_isPrepare)
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("cannot PREPARE a transaction that has created a cursor WITH HOLD")));

			fc_HoldPortal(fc_portal);

			/* 报告我们改变了状态 */
			fc_result = true;
		}
		else if (fc_portal->createSubid == InvalidSubTransactionId)
		{
			/*
			 * 对于从先前事务保留的游标，不做任何操作
			 * （包括我们在此循环的先前周期中刚冻结的那些）。
			 */
			continue;
		}
		else
		{
			/* 清除所有非可持有的门户 */
			PortalDrop(fc_portal, true);

			/* 报告我们改变了状态 */
			fc_result = true;
		}

		/*
		 * 在冻结或丢弃门户之后，我们必须重新开始
		 * 迭代，因为我们可能已经调用了用户定义的代码，导致
		 * 丢弃了哈希链中的下一个门户。
		 */
		hash_seq_term(&fc_status);
		hash_seq_init(&fc_status, PortalHashTable);
	}

	return fc_result;
}

/*
 * 中止门户的处理。
 *
 * 此时，如果存在，我们会运行清理钩子，但在清理调用之前
 * 我们无法释放门户的内存。
 */
void AtAbort_Portals(void)
{
	HASH_SEQ_STATUS fc_status;
	PortalHashEnt *fc_hentry;

	hash_seq_init(&fc_status, PortalHashTable);

	while ((fc_hentry = (PortalHashEnt *) hash_seq_search(&fc_status)) != NULL)
	{
		Portal		fc_portal = fc_hentry->portal;

		/*
		 * 当 elog(FATAL) 进展时，我们需要将活动门户设置为
		 * 失败，以便 PortalCleanup() 不会运行执行器关闭。
		 */
		if (fc_portal->status == PORTAL_ACTIVE && shmem_exit_inprogress)
			MarkPortalFailed(fc_portal);

		/*
		 * 对于从先前事务保留的游标不做其他操作。
		 */
		if (fc_portal->createSubid == InvalidSubTransactionId)
			continue;

		/*
		 * 对于自动保持的游标不做任何操作。这类似于来自
		 * 先前事务的游标的情况，但也可能是该游标在此事务中
		 * 被自动保持，因此它希望继续存在。
		 */
		if (fc_portal->autoHeld)
			continue;

		/*
		 * 如果是在当前事务中创建的，我们也无法对 READY
		 * 门户执行正常关闭；它可能引用在失败事务中创建的对象。
		 * 请参见 AtSubAbort_Portals 中的评论。
		 */
		if (fc_portal->status == PORTAL_READY)
			MarkPortalFailed(fc_portal);

		/*
		 * 允许 portalcmds.c 清理它所知道的状态，如果我们
		 * 还没有这样做。
		 */
		if (PointerIsValid(fc_portal->cleanup))
		{
			fc_portal->cleanup(fc_portal);
			fc_portal->cleanup = NULL;
		}

		/* 删除缓存的计划引用（如果有的话） */
		fc_PortalReleaseCachedPlan(fc_portal);

		/*
		 * 所有属于门户的资源将在即将进行的事务广泛清理中释放；
		 * 在我们运行 PortalDrop 之前，它们将消失。
		 */
		fc_portal->resowner = NULL;

		/*
		 * 虽然我们无法删除门户数据结构本身，但我们可以
		 * 释放子上下文中的任何内存，例如执行器状态。
		 * 清理钩子是最后一个可能需要那里的数据的东西。
		 * 但请保持活动门户的状态不变。
		 */
		if (fc_portal->status != PORTAL_ACTIVE)
			MemoryContextDeleteChildren(fc_portal->portalContext);
	}
}

/*
 * 事务中止后对门户进行清理。
 *
 * 删除所有未保留自先前事务的门户。 */
void AtCleanup_Portals(void)
{
	HASH_SEQ_STATUS fc_status;
	PortalHashEnt *fc_hentry;

	hash_seq_init(&fc_status, PortalHashTable);

	while ((fc_hentry = (PortalHashEnt *) hash_seq_search(&fc_status)) != NULL)
	{
		Portal		fc_portal = fc_hentry->portal;

		/*
		 * 不要触及活动门户——这只能发生在
		 * 多事务命令的情况下。
		 */
		if (fc_portal->status == PORTAL_ACTIVE)
			continue;

		/*
		 * 对于从先前事务保留的游标或自动持有的游标不做任何操作。
		 */
		if (fc_portal->createSubid == InvalidSubTransactionId || fc_portal->autoHeld)
		{
			Assert(fc_portal->status != PORTAL_ACTIVE);
			Assert(fc_portal->resowner == NULL);
			continue;
		}

		/*
		 * 如果门户仍然被锁定，强制解除锁定。否则，PortalDrop
		 * 将不允许我们丢弃门户。锁定门户的用户在中止时
		 * 也被中断，并且不会再尝试使用它。
		 */
		if (fc_portal->portalPinned)
			fc_portal->portalPinned = false;

		/*
		 * 我们最好在清理期间不调用任何用户定义的代码，
		 * 因此如果清理钩子还未运行，那也没办法；我们只能跳过它。
		 */
		if (PointerIsValid(fc_portal->cleanup))
		{
			elog(WARNING, "skipping cleanup for portal \"%s\"", fc_portal->name);
			fc_portal->cleanup = NULL;
		}

		/* Zap it. */
		PortalDrop(fc_portal, false);
	}
}

/*
 * 在错误时返回主循环时与门户相关的清理。
 *
 * 这与事务中止时的清理不同。自动持有的门户
 * 在出错时清理，但在事务中止时不清理。
 */
void PortalErrorCleanup(void)
{
	HASH_SEQ_STATUS fc_status;
	PortalHashEnt *fc_hentry;

	hash_seq_init(&fc_status, PortalHashTable);

	while ((fc_hentry = (PortalHashEnt *) hash_seq_search(&fc_status)) != NULL)
	{
		Portal		fc_portal = fc_hentry->portal;

		if (fc_portal->autoHeld)
		{
			fc_portal->portalPinned = false;
			PortalDrop(fc_portal, false);
		}
	}
}

/*
 * 门户的预提交处理。
 *
 * 将当前子事务中创建或使用的门户重新分配给
 * 父子事务。
 */
void AtSubCommit_Portals(SubTransactionId fc_mySubid,
					SubTransactionId fc_parentSubid,
					int fc_parentLevel,
					ResourceOwner fc_parentXactOwner)
{
	HASH_SEQ_STATUS fc_status;
	PortalHashEnt *fc_hentry;

	hash_seq_init(&fc_status, PortalHashTable);

	while ((fc_hentry = (PortalHashEnt *) hash_seq_search(&fc_status)) != NULL)
	{
		Portal		fc_portal = fc_hentry->portal;

		if (fc_portal->createSubid == fc_mySubid)
		{
			fc_portal->createSubid = fc_parentSubid;
			fc_portal->createLevel = fc_parentLevel;
			if (fc_portal->resowner)
				ResourceOwnerNewParent(fc_portal->resowner, fc_parentXactOwner);
		}
		if (fc_portal->activeSubid == fc_mySubid)
			fc_portal->activeSubid = fc_parentSubid;
	}
}

/*
 * 子事务中止处理门户。
 *
 * 禁用在失败的子事务期间创建或使用的门户。
 * 请注意，根据 AtSubCommit_Portals，这也会捕捉到在
 * 子事务的后代中创建/使用的门户。
 *
 * 我们在这里不销毁任何门户；那是在 AtSubCleanup_Portals 中处理的。
 */
void AtSubAbort_Portals(SubTransactionId fc_mySubid,
				   SubTransactionId fc_parentSubid,
				   ResourceOwner fc_myXactOwner,
				   ResourceOwner fc_parentXactOwner)
{
	HASH_SEQ_STATUS fc_status;
	PortalHashEnt *fc_hentry;

	hash_seq_init(&fc_status, PortalHashTable);

	while ((fc_hentry = (PortalHashEnt *) hash_seq_search(&fc_status)) != NULL)
	{
		Portal		fc_portal = fc_hentry->portal;

		/* 是否在这个子事务中创建的？ */
		if (fc_portal->createSubid != fc_mySubid)
		{
			/* 不，但也许在这个子事务中使用过？ */
			if (fc_portal->activeSubid == fc_mySubid)
			{
				/* 保持 activeSubid，直到门户被移除 */
				fc_portal->activeSubid = fc_parentSubid;

				/*
				 * MarkPortalActive() 调用者在
				 * 这个子事务中运行了上层门户，并将门户保持为 ACTIVE。
				 * 这不应该发生，但因上述原因强制将门户状态
				 * 设置为 FAILED。
				 *
				 * 我们假设即使被运行然后挂起的上层 READY
				 * 门户也可以不被强制失败。从理论上讲，一个挂起的
				 * 上层门户可能已经获得了一些即将被销毁的对象的
				 * 引用，但对此类情况应该有足够的防御措施：
				 * 门户的原始查询不能包含此类引用，且诸如 PL/pgSQL
				 * 函数的缓存计划中的任何引用都不是来自活动查询，
				 * 应该由重新验证逻辑保护。
				 */
				if (fc_portal->status == PORTAL_ACTIVE)
					MarkPortalFailed(fc_portal);

				
/*
				 * 另外，如果我们在当前子事务中失败了
				 * （无论是在上面还是更早），将其资源
				 * 拥有者附加到当前子事务的资源拥有者，这样
				 * 它仍然持有的任何资源将在清理
				 * 此子事务时被释放。这防止了一些边缘情况，
				 * 在这些情况下，我们可能会在清理
				 * 当前子事务中创建的对象时遇到断言或更糟糕的情况
				 * （因为它们仍然在这个门户中被引用）。
				 */
				if (fc_portal->status == PORTAL_FAILED && fc_portal->resowner)
				{
					ResourceOwnerNewParent(fc_portal->resowner, fc_myXactOwner);
					fc_portal->resowner = NULL;
				}
			}
			/* 如果它不是在这个子事务中创建的，则完成 */
			continue;
		}

		/*
		 * 强制我自己的子事务中的任何活动门户进入失败状态。
		 * 我们必须这样做，因为它们可能引用在失败的子事务中创建或
		 * 更改的对象，从而导致在portalcmds.c试图关闭
		 * 门户时导致ExecutorEnd崩溃。
		 * 目前，所有MarkPortalActive()调用者确保在放弃控制前
		 * 再次更新门户状态，因此这里不能发生激活。如果发生，
		 * 像现有的MarkPortalActive()调用者一样处理门户。
		 */
		if (fc_portal->status == PORTAL_READY ||
			fc_portal->status == PORTAL_ACTIVE)
			MarkPortalFailed(fc_portal);

		/*
		 * 允许 portalcmds.c 清理它所知道的状态，如果我们
		 * 还没有这样做。
		 */
		if (PointerIsValid(fc_portal->cleanup))
		{
			fc_portal->cleanup(fc_portal);
			fc_portal->cleanup = NULL;
		}

		/* 删除缓存的计划引用（如果有的话） */
		fc_PortalReleaseCachedPlan(fc_portal);

		/*
		 * 所有属于门户的资源将在即将进行的事务广泛清理中释放；
		 * 在我们运行 PortalDrop 之前，它们将消失。
		 */
		fc_portal->resowner = NULL;

		/*
		 * 虽然我们不能删除门户数据结构本身，但我们可以
		 * 释放任何子上下文中的内存，例如执行器状态。
		 * 清理钩子是最后一个可能需要数据的地方。
		 */
		MemoryContextDeleteChildren(fc_portal->portalContext);
	}
}

/*
 * 事务回滚后的门户清理。
 *
 * 删除在失败的子事务中创建的所有门户（但注意
 * 我们不会删除任何已经重新分配给上级的门户）。
 */
void AtSubCleanup_Portals(SubTransactionId fc_mySubid)
{
	HASH_SEQ_STATUS fc_status;
	PortalHashEnt *fc_hentry;

	hash_seq_init(&fc_status, PortalHashTable);

	while ((fc_hentry = (PortalHashEnt *) hash_seq_search(&fc_status)) != NULL)
	{
		Portal		fc_portal = fc_hentry->portal;

		if (fc_portal->createSubid != fc_mySubid)
			continue;

		/*
		 * 如果门户仍然被锁定，强制解除锁定。否则，PortalDrop
		 * 将不允许我们丢弃门户。锁定门户的用户在中止时
		 * 也被中断，并且不会再尝试使用它。
		 */
		if (fc_portal->portalPinned)
			fc_portal->portalPinned = false;

		/*
		 * 我们最好在清理期间不调用任何用户定义的代码，
		 * 因此如果清理钩子还未运行，那也没办法；我们只能跳过它。
		 */
		if (PointerIsValid(fc_portal->cleanup))
		{
			elog(WARNING, "skipping cleanup for portal \"%s\"", fc_portal->name);
			fc_portal->cleanup = NULL;
		}

		/* Zap it. */
		PortalDrop(fc_portal, false);
	}
}

/* 查找所有可用的游标 */
Datum pg_cursor(PG_FUNCTION_ARGS)
{
	ReturnSetInfo *fc_rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
	HASH_SEQ_STATUS fc_hash_seq;
	PortalHashEnt *fc_hentry;

	/*
	 * 我们将所有元组放入一个元组存储中，通过一次扫描哈希表。
	 * 这避免了在调用之间哈希表可能发生变化的问题。
	 */
	InitMaterializedSRF(fcinfo, 0);

	hash_seq_init(&fc_hash_seq, PortalHashTable);
	while ((fc_hentry = hash_seq_search(&fc_hash_seq)) != NULL)
	{
		Portal		fc_portal = fc_hentry->portal;
		Datum		fc_values[6];
		bool		fc_nulls[6];

		/* 仅报告“可见”的条目 */
		if (!fc_portal->visible)
			continue;
		/* 如果还没有调用 PortalDefineQuery，也要忽略它 */
		if (!fc_portal->sourceText)
			continue;

		MemSet(fc_nulls, 0, sizeof(fc_nulls));

		fc_values[0] = CStringGetTextDatum(fc_portal->name);
		fc_values[1] = CStringGetTextDatum(fc_portal->sourceText);
		fc_values[2] = BoolGetDatum(fc_portal->cursorOptions & CURSOR_OPT_HOLD);
		fc_values[3] = BoolGetDatum(fc_portal->cursorOptions & CURSOR_OPT_BINARY);
		fc_values[4] = BoolGetDatum(fc_portal->cursorOptions & CURSOR_OPT_SCROLL);
		fc_values[5] = TimestampTzGetDatum(fc_portal->creation_time);

		tuplestore_putvalues(fc_rsinfo->setResult, fc_rsinfo->setDesc, fc_values, fc_nulls);
	}

	return (Datum) 0;
}

bool ThereAreNoReadyPortals(void)
{
	HASH_SEQ_STATUS fc_status;
	PortalHashEnt *fc_hentry;

	hash_seq_init(&fc_status, PortalHashTable);

	while ((fc_hentry = (PortalHashEnt *) hash_seq_search(&fc_status)) != NULL)
	{
		Portal		fc_portal = fc_hentry->portal;

		if (fc_portal->status == PORTAL_READY)
			return false;
	}

	return true;
}

/*
 * 持有所有固定的门户。
 *
 * 在程序内部发起 COMMIT 或 ROLLBACK 时，必须调用
 * 这个函数以保护内部生成的游标在
 * 事务关闭期间不被删除。目前，SPI 自动调用此
 * 函数；以其他方式发起 COMMIT 或 ROLLBACK 的 PL
 * 需要自己处理。（注意，我们无法在，例如，AtAbort_Portals
 * 中执行此操作，因为我们需要在持久化门户时运行用户定义的代码。
 * 一旦事务回滚开始，就太迟了。）
 *
 * 我们通过将这些门户转换为持有的游标来保护它们。我们将它们
 * 标记为“自动持有”，以便异常退出时知道清理它们。（在正常、
 * 非异常代码路径中，PL 需要自己清理这些门户，因为
 * 事务结束后将不再执行此操作；但这应该是正常流程
 * 。）
 */
void HoldPinnedPortals(void)
{
	HASH_SEQ_STATUS fc_status;
	PortalHashEnt *fc_hentry;

	hash_seq_init(&fc_status, PortalHashTable);

	while ((fc_hentry = (PortalHashEnt *) hash_seq_search(&fc_status)) != NULL)
	{
		Portal		fc_portal = fc_hentry->portal;

		if (fc_portal->portalPinned && !fc_portal->autoHeld)
		{
			/*
			 * 在非只读的游标循环中进行事务控制，特别是回滚， 
			 * 例如使用 UPDATE ...
			 * RETURNING，会出现奇怪的语义问题。此外，这个
			 * 实现也无法工作，因为这样的门户不能被
			 * 持有。（核心语法强制要求只有 SELECT 语句
			 * 可以驱动游标，但例如 PL/pgSQL 并不限制
			 * 这一点。）
			 */
			if (fc_portal->strategy != PORTAL_ONE_SELECT)
				ereport(ERROR,
						(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
						 errmsg("cannot perform transaction commands inside a cursor loop that is not read-only")));

			/* 验证其处于可持有状态 */
			if (fc_portal->status != PORTAL_READY)
				elog(ERROR, "pinned portal is not ready to be auto-held");

			fc_HoldPortal(fc_portal);
			fc_portal->autoHeld = true;
		}
	}
}

/*
 * 删除所有门户的外部活动快照，以便没有快照
 * 保持活动。
 *
 * 与 HoldPinnedPortals 一样，在程序内部发起 COMMIT 或
 * ROLLBACK 时也必须调用这个。它必须与之分开，因为
 * 在我们完成可能会失败的步骤之前不应该运行它。
 *
 * 将其合并到 PreCommit_Portals 可能很诱人，但如果这样做，
 * 我们需要清理 VACUUM 中的快照管理，以及可能的其他地方。
 */
void ForgetPortalSnapshots(void)
{
	HASH_SEQ_STATUS fc_status;
	PortalHashEnt *fc_hentry;
	int			fc_numPortalSnaps = 0;
	int			fc_numActiveSnaps = 0;

	/* 首先，扫描 PortalHashTable 并清除 portalSnapshot 字段 */
	hash_seq_init(&fc_status, PortalHashTable);

	while ((fc_hentry = (PortalHashEnt *) hash_seq_search(&fc_status)) != NULL)
	{
		Portal		fc_portal = fc_hentry->portal;

		if (fc_portal->portalSnapshot != NULL)
		{
			fc_portal->portalSnapshot = NULL;
			fc_numPortalSnaps++;
		}
		/* portal->holdSnapshot 将在 PreCommit_Portals 中清理 */
	}

	/*
	 * 现在，弹出所有活动快照，这些快照应该只是
	 * 门户快照。理想情况下，我们会直接从门户
	 * 扫描中驱动这一点，但没有好的方法以正确顺序访问
	 * 门户。所以只是事后交叉检查。
	 */
	while (ActiveSnapshotSet())
	{
		PopActiveSnapshot();
		fc_numActiveSnaps++;
	}

	if (fc_numPortalSnaps != fc_numActiveSnaps)
		elog(ERROR, "portal snapshots (%d) did not account for all active snapshots (%d)",
			 fc_numPortalSnaps, fc_numActiveSnaps);
}
